Skip to content

feat: expire stale payment-link intents and detect amount mismatches - #205

Merged
Emmyt24 merged 1 commit into
mainfrom
feat/payment-link-expiry-mismatch-fixes
Aug 3, 2026
Merged

feat: expire stale payment-link intents and detect amount mismatches#205
Emmyt24 merged 1 commit into
mainfrom
feat/payment-link-expiry-mismatch-fixes

Conversation

@Emmyt24

@Emmyt24 Emmyt24 commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

Summary

Two real gaps found in live testing of the payment-link checkout:

  1. Stale pending payments never expire. A payment intent that never completes stayed pending in the DB forever — the status CHECK only allowed pending/confirmed, and nothing ever swept stale rows. Adds an expired status; the ingest supervisor's tick() now sweeps pending intents older than 1 hour on every pass (a single indexed UPDATE, cheap even when it matches nothing) and fires a payment_link.expired webhook per swept row.

  2. Wrong-amount deposits were silently absorbed. A manual/bank-style transfer sending too little or too much: the existing underpayment guard just logged a server-side warning and left the payment pending forever — no webhook, no payer-facing signal. Overpayment wasn't checked at all; it silently confirmed as if correct. Adds underpaid/overpaid statuses, a payment_link.mismatched webhook carrying both the expected and received amounts, and the mismatched deposit's transaction stays linked (so the merchant can see what actually arrived) without ever being marked confirmed.

  • Migration 0018: widens the payment_link_payments.status CHECK.
  • crates/store: expire_stale_payment_link_payments, mark_payment_link_payment_mismatched, get_payment_link_by_id (unscoped, for the sweep's internal webhook lookup), get_transaction.
  • crates/api: PaymentStatusView gains expected_usdc_stroops/received_usdc_stroops so the pay page can render a mismatch banner without a second request.

Companion frontend PR wires required payer fields, an email-format check, a back button, and the new mismatch/expired states into the hosted pay page.

Test plan

  • cargo test --workspace --locked — all green (store: sweep marks only >1hr-old pending rows and is idempotent; ingest: underpaid/overpaid/exact deposits each land on the correct status via a real Ingestor::process() call)
  • cargo clippy --workspace --all-targets --locked -- -D warnings — clean
  • cargo fmt --all -- --check — clean

Two real gaps found in live testing:

1. A payment intent that never completes stayed 'pending' in the DB
   forever -- the status CHECK only allowed pending/confirmed and
   nothing ever swept stale rows. Adds an 'expired' status; the
   ingest supervisor's tick() now sweeps pending intents older than
   1 hour on every pass (a single indexed UPDATE, cheap even when it
   matches nothing) and fires a payment_link.expired webhook per row.

2. A deposit for the wrong amount (manual bank-style transfer sending
   too little or too much) was silently absorbed: the existing
   underpayment guard just logged a server-side warning and left the
   payment pending forever, with no webhook and no payer-facing
   signal. Overpayment wasn't checked at all -- it silently confirmed
   as if correct. Adds 'underpaid'/'overpaid' statuses, a
   payment_link.mismatched webhook carrying both the expected and
   received amounts, and the mismatch's transaction stays linked (so
   the merchant can see what actually arrived) without ever being
   marked confirmed.

- migration 0018: widens the payment_link_payments status CHECK.
- crates/store: expire_stale_payment_link_payments,
  mark_payment_link_payment_mismatched, get_payment_link_by_id (unscoped,
  for the sweep's internal webhook lookup), get_transaction.
- crates/api: PaymentStatusView gains expected_usdc_stroops/
  received_usdc_stroops so the pay page can render a mismatch banner
  without a second request.
- Tests: sweep marks only >1hr-old pending rows and is idempotent;
  underpaid/overpaid/exact deposits each land on the correct status
  via a real Ingestor::process() call.
@Emmyt24
Emmyt24 merged commit 76dbaa4 into main Aug 3, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant